package ants;

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.util.regex.Pattern;

import ants.map.Coordinate;
import ants.map.Direction;

/**
 * Handles system input stream parsing.
 */
public abstract class AbstractSystemInputParser extends
  AbstractSystemInputReader {

  private enum SetupToken {
    ATTACKRADIUS2, COLS, LOADTIME, ROWS, SPAWNRADIUS2, TURNS, TURNTIME,
    VIEWRADIUS2;

    private static final Pattern PATTERN = compilePattern(SetupToken.class);
  }

  private enum UpdateToken {
    A, D, F, H, W;

    private static final Pattern PATTERN = compilePattern(UpdateToken.class);
  }

  private static final char COMMENT_CHAR = '#';
  private static final String GO = "go";
  private static final String READY = "ready";

  private static Pattern compilePattern(Class<? extends Enum<?>> clazz) {
    StringBuilder builder = new StringBuilder("(");
    for (Enum<?> enumConstant : clazz.getEnumConstants()) {
      if (enumConstant.ordinal() > 0) {
        builder.append("|");
      }
      builder.append(enumConstant.name());
    }
    builder.append(")");
    return Pattern.compile(builder.toString());
  }

  private final List<String> input = new ArrayList<String>();

  protected int turn = 0;

  public abstract void addAnt(int row, int col, int owner);

  public abstract void addFood(int row, int col);

  public abstract void addHill(int row, int col, int owner);

  public abstract void addWater(int row, int col);

  /**
   * Enables performing actions which should take place just after the game
   * state has been updated.
   */
  public abstract void afterUpdate();

  public abstract void beforeSetup();

  /**
   * Enables performing actions which should take place prior to updating the
   * game state, like clearing old game data.
   */
  public abstract void beforeUpdate();

  /**
   * Subclasses are supposed to use this method to process the game state and
   * send orders.
   */
  public abstract void doTurn();

  /**
   * Finishes turn.
   */
  public void finishTurn() {
    System.out.println("go");
    System.out.flush();
  }

  public void issueOrder(Coordinate coordinate, Direction direction) {
    String order =
      String
        .format("o %s %s %s", coordinate.row(), coordinate.col(), direction);
    System.out.println(order);
    System.out.flush();
  }

  private boolean isTurnLine(String line) {
    return line.matches("turn \\d+");
  }

  /**
   * Parses the setup information from system input stream.
   */
  public void parseSetup(List<String> input) {
    beforeSetup();
    int loadTime = 0;
    int turnTime = 0;
    int rows = 0;
    int cols = 0;
    int turns = 0;
    int viewRadius2 = 0;
    int attackRadius2 = 0;
    int spawnRadius2 = 0;
    for (String line : input) {
      line = removeComment(line);
      if (line.isEmpty()) {
        continue;
      }

      if (isTurnLine(line)) {
        recordTurn(line);
        continue;
      }

      Scanner scanner = new Scanner(line);
      if (!scanner.hasNext()) {
        continue;
      }
      String token = scanner.next().toUpperCase();
      if (!SetupToken.PATTERN.matcher(token).matches()) {
        continue;
      }
      SetupToken setupToken = SetupToken.valueOf(token);
      switch (setupToken) {
        case LOADTIME:
          loadTime = scanner.nextInt();
          break;
        case TURNTIME:
          turnTime = scanner.nextInt();
          break;
        case ROWS:
          rows = scanner.nextInt();
          break;
        case COLS:
          cols = scanner.nextInt();
          break;
        case TURNS:
          turns = scanner.nextInt();
          break;
        case VIEWRADIUS2:
          viewRadius2 = scanner.nextInt();
          break;
        case ATTACKRADIUS2:
          attackRadius2 = scanner.nextInt();
          break;
        case SPAWNRADIUS2:
          spawnRadius2 = scanner.nextInt();
          break;
      }
    }
    setup(loadTime, turnTime, rows, cols, turns, viewRadius2, attackRadius2,
      spawnRadius2);
  }

  /**
   * Parses the update information from system input stream.
   * 
   * @param input
   *          update information
   */
  public void parseUpdate(List<String> input) {
    beforeUpdate();
    for (String line : input) {
      line = removeComment(line);
      if (line.isEmpty()) {
        continue;
      }
      if (isTurnLine(line)) {
        recordTurn(line);
        continue;
      }
      Scanner scanner = new Scanner(line);
      if (!scanner.hasNext()) {
        continue;
      }
      String token = scanner.next().toUpperCase();
      if (!UpdateToken.PATTERN.matcher(token).matches()) {
        continue;
      }
      UpdateToken updateToken = UpdateToken.valueOf(token);
      int row = scanner.nextInt();
      int col = scanner.nextInt();
      switch (updateToken) {
        case W:
          addWater(row, col);
          break;
        case A:
          if (scanner.hasNextInt()) {
            addAnt(row, col, scanner.nextInt());
          }
          break;
        case F:
          addFood(row, col);
          break;
        case D:
          if (scanner.hasNextInt()) {
            removeAnt(row, col, scanner.nextInt());
          }
          break;
        case H:
          if (scanner.hasNextInt()) {
            addHill(row, col, scanner.nextInt());
          }
          break;
      }
    }
    afterUpdate();
  }

  /**
   * Collects lines read from system input stream until a keyword appears and
   * then parses them.
   */
  @Override
  public void processLine(String line) {
    if (line.equals(READY)) {
      parseSetup(input);
      doTurn();
      finishTurn();
      input.clear();
    } else if (line.equals(GO)) {
      parseUpdate(input);
      doTurn();
      finishTurn();
      input.clear();
    } else if (!line.isEmpty()) {
      input.add(line);
    }
  }

  private void recordTurn(String line) {
    turn = Integer.parseInt(line.split(" ")[1]);
  }

  public abstract void removeAnt(int row, int col, int owner);

  private String removeComment(String line) {
    int commentCharIndex = line.indexOf(COMMENT_CHAR);
    String lineWithoutComment;
    if (commentCharIndex >= 0) {
      lineWithoutComment = line.substring(0, commentCharIndex).trim();
    } else {
      lineWithoutComment = line;
    }
    return lineWithoutComment;
  }

  /**
   * Sets up the game state.
   * 
   * @param loadTime
   *          timeout for initializing and setting up the bot on turn 0
   * @param turnTime
   *          timeout for a single game turn, starting with turn 1
   * @param rows
   *          game map height
   * @param cols
   *          game map width
   * @param turns
   *          maximum number of turns the game will be played
   * @param viewRadius2
   *          squared view radius of each ant
   * @param attackRadius2
   *          squared attack radius of each ant
   * @param spawnRadius2
   *          squared spawn radius of each ant
   */
  public abstract void setup(int loadTime, int turnTime, int rows, int cols,
    int turns, int viewRadius2, int attackRadius2, int spawnRadius2);
}
